using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using MazeBuilder.Entities;
using MazeBuilder.Entities.Tiles;
using RoomModuleCatalog;

namespace MazeBuilder
{
  public class StandardDigger : Digger
  {
    protected Maze m { get; set; }

    protected override void manageBounders(Maze maze, Dictionary<Direction, bool> bounders, int i, int j, int startRow, int startCol) {
      var c = maze._configuration;

      if (i != (startRow + c.RoomHeight / 2) && j != (startCol + c.RoomWidth / 2)) return;

      var sopra = maze.tiles[i - 1][j];
      var sotto = maze.tiles[i + 1][j];
      var sx = maze.tiles[i][j - 1];
      var dx = maze.tiles[i][j + 1];

      if (sopra is WallTile && sotto is WallTile)
        if (sx is NullTile || dx is NullTile)
          maze.tiles[i][j] = new WallTile(i, j) { ID = -1 };

        else
          maze.tiles[i][j] = new FloorTile(i, j) { ID = -1 };

      if (sx is WallTile && dx is WallTile)
        if (sopra is NullTile || sotto is NullTile)
          maze.tiles[i][j] = new WallTile(i, j) { ID = -1 };

        else
          maze.tiles[i][j] = new FloorTile(i, j) { ID = -1 };
    }

    /// <exception cref="ArgumentOutOfRangeException"><c></c> is out of range.</exception>
    private static Point GetCoords(IMazeConfiguration config, Direction direction, int row, int column) {
      var coords = new Point(-1, -1);
      switch (direction) {
        case Direction.North:
          coords.X = row - config.RoomHeight - 1;
          coords.Y = column;
          break;
        case Direction.East:
          coords.X = row;
          coords.Y = column + config.RoomWidth + 1;
          break;
        case Direction.South:
          coords.X = row + config.RoomHeight + 1;
          coords.Y = column;
          break;
        case Direction.West:
          coords.X = row;
          coords.Y = column - config.RoomWidth - 1;
          break;
        default:
          throw new ArgumentOutOfRangeException();
      }
      return coords;
    }

    private Tile NextTile(int roomID) {

      var c = m._configuration;

      var t = m.tiles.SelectMany(list => list).ToList();
      int row, col;

      if (roomID > 0) {
        var g = t.Where(x => x.ID == roomID);

        var enumerable = g as Tile[] ?? g.ToArray();
        var minRow = enumerable.Min(x => x.Row);
        var minColumn = enumerable.Min(x => x.Col);

        row = minRow + (c.RoomHeight / 2);
        col = minColumn + (c.RoomWidth / 2);
      }
      else
        return m.FindTile(((c.MaxRoomNumber + 10) * c.RoomHeight) / 2, (c.MaxRoomNumber + 10) * c.RoomWidth / 2);

      var direction = (Direction)StaticRandomNumberGenerator.Next(1, Enum.GetValues(typeof(Direction)).Length);

      Tile result = null;
      var coords = GetCoords(c, direction, row, col);

      if (coords.X >= 0 &&
          coords.X < (c.MaxRoomNumber + 10) * c.RoomHeight &&
          coords.Y >= 0 &&
          coords.Y < (c.MaxRoomNumber + 10) * c.RoomWidth)
        result = m.FindTile(coords.X, coords.Y);

      if (!(result is NullTile))
        result = null;

      return result;
    }

    private int PickRandomRoom() {
      return m.RoomCounter == 0 ? m.RoomCounter : StaticRandomNumberGenerator.Next(1, m.RoomCounter);
    }

    #region Overrides of Digger

    public override void Build(Maze maze) {
      m = maze;
      PlaceRooms(maze);
      PlaceDoors(maze);
      CleanClosedRooms(maze);

      OnBuilded(new buildedArgs());
    }

    private static void CleanClosedRooms(Maze maze) {
      foreach (var key in maze.ClosedByDoors) {
        CleanRoom(maze, key);
      }
    }

    private static void PlaceDoors(Maze maze) {
      var m = maze;
      var c = maze._configuration;

      var placedRooms = m.tiles.SelectMany(list => list).Where(i => i.ID > 0).GroupBy(j => j.ID).OrderBy(k => k.Min(x => x.Row)).ThenBy(f => f.Min(x=> x.Col));
      foreach (var room in placedRooms) {
        var minRow = room.Min(x => x.Row);
        var minCol = room.Min(x => x.Col);

        var dirs = new Dictionary<Direction, Tile>
                                            {
                                              {Direction.North, m.tiles[minRow - 1][minCol + (c.RoomWidth / 2)]},  
                                              {Direction.South, m.tiles[minRow + c.RoomHeight][minCol + (c.RoomWidth / 2)]},  
                                              {Direction.East, m.tiles[minRow + (c.RoomHeight / 2)][minCol - 1]},  
                                              {Direction.West, m.tiles[minRow + (c.RoomHeight / 2)][minCol + c.RoomWidth]},  
                                            };
        var openDirs = dirs.Where(x => x.Value is FloorTile).ToArray();

        if (!openDirs.Any()) continue;

        ScrambleDirections(ref openDirs);
        var pDoors = new List<KeyValuePair<Direction, Tile>>();
        var pWalls = new List<KeyValuePair<Direction, Tile>>();
        var st = new Stack<KeyValuePair<Direction, Tile>>();

        foreach (var d in openDirs)
          st.Push(d);

        if (openDirs.Count() == 1) {
          if (checkForAddDoor(getOpenDirectionBounders(m, openDirs[0].Value)))
          {
            maze.ClosedByDoors.Add(room.Key);
            pDoors.Add(st.Pop());
          }
        }

        for (var i = 0; i < st.Count; i++)
          if (StaticRandomNumberGenerator.Next(1, 100) <= 25)
            pWalls.Add(st.Pop());

        foreach (var door in pDoors)
        {
          if (maze.PlacedDoors <= maze.MaxDoors)
          {
            m.tiles[door.Value.Row][door.Value.Col] = new DoorTile(door.Value.Row, door.Value.Col) {ID = -1};
            maze.PlacedDoors++;
          }
        }

        foreach (var wall in pWalls)
          if (StaticRandomNumberGenerator.Next(1, 100) <= 100)
            m.tiles[wall.Value.Row][wall.Value.Col] = new WallTile(wall.Value.Row, wall.Value.Col) { ID = -1 };
      }
    }

    private static bool checkForAddDoor(Dictionary<Direction, Tile> openDirBounder)
    {
      bool addDoor = false;
      if (openDirBounder[Direction.North].Code == (int) TileCode.Wall &&
          openDirBounder[Direction.South].Code == (int) TileCode.Wall)
      {
        if (openDirBounder[Direction.East].ID > 0 && openDirBounder[Direction.West].ID > 0)
        {
          addDoor = true;
        }
      }
      else if (openDirBounder[Direction.East].Code == (int) TileCode.Wall &&
               openDirBounder[Direction.West].Code == (int) TileCode.Wall)
      {
        if (openDirBounder[Direction.North].ID > 0 && openDirBounder[Direction.South].ID > 0)
        {
          addDoor = true;
        }
      }
      return addDoor;
    }

    private static Dictionary<Direction, Tile> getOpenDirectionBounders(Maze m, Tile lonely)
    {
      return new Dictionary<Direction, Tile>
               {
                 {
                   Direction.North, m.FindTile(lonely.Row -1, lonely.Col)
                 },
                 {
                   Direction.South, m.FindTile(lonely.Row +1, lonely.Col)
                 },                           
                 {
                   Direction.East, m.FindTile(lonely.Row, lonely.Col +1)
                 },                           
                 {
                   Direction.West, m.FindTile(lonely.Row, lonely.Col +1)
                 },                           
               };
    }

    private static void CleanRoom(Maze maze, int key) {
      var room =
        maze.tiles.SelectMany(list => list, (list, l) => new { list, l }).Where(
          @t => @t.l.ID == key).Select(@t => @t.l).ToList();

      foreach (var tile in room)
      {
        var floorTile = new FloorTile(tile.Row, tile.Col) {ID = tile.ID};
        maze.tiles[tile.Row][tile.Col] = floorTile;
      }
    }

    private void PlaceRooms(Maze maze) {
      if (maze.RoomCounter == maze._configuration.MaxRoomNumber)
        return;
      DigRoom(maze, NextTile(PickRandomRoom()));
      PlaceRooms(maze);
    }

    public override void DigRoom(Maze maze, Tile tile) {
      if (tile == null)
        return;
      var c = maze._configuration;
      var startRow = tile.Row - (c.RoomHeight / 2);
      var startCol = tile.Col - (c.RoomWidth / 2);
      var currX = startRow + c.RoomHeight + 1;
      var currY = startCol + c.RoomWidth + 1;
      var lengthX = maze.tiles.GetLength(0);
      var lengthY = maze.tiles[0].GetLength(0);

      if (startRow - 1 < 0 || startCol - 1 < 0 || currX >= lengthX || currY >= lengthY)
        return;

      maze.RoomCounter++;
      var roomDefinition = RoomFactory.GetRoomDefinition(startRow, currX - 1, startCol, currY - 1, maze.RoomCounter);

      var currentI = 0;
      for (var i = startRow - 1; i < currX; i++) {
        var currentJ = 0;
        var incrementI = false;
        for (var j = startCol - 1; j < currY; j++) {
          if (i == startRow - 1 || i == startRow + c.RoomHeight || j == startCol - 1 || j == startCol + c.RoomWidth)
            maze.tiles[i][j] = new WallTile(i, j) { ID = -1 };
          else {
            maze.tiles[i][j] = roomDefinition.GetTile(currentI, currentJ, maze.RoomCounter);
            maze.tiles[i][j].Visited = true;
            currentJ++;
            incrementI = true;
          }
        }
        if (incrementI)
          currentI++;
      }

      var bounders = getBounders(tile, maze);
      for (var i = startRow - 1; i < currX; i++)
        for (var j = startCol - 1; j < currY; j++)
          if (i == startRow - 1 || i == startRow + c.RoomHeight || j == startCol - 1 || j == startCol + c.RoomWidth)
            manageBounders(maze, bounders, i, j, startRow, startCol);

      OnOneBuilded(new RoomBuildedArgs());
    }
    #endregion

    private static void ScrambleDirections(ref KeyValuePair<Direction, Tile>[] openDirs) {
      for (var i = 0; i < 10; i++) {
        var p = StaticRandomNumberGenerator.Next(0, openDirs.Count() - 1);
        var pp = StaticRandomNumberGenerator.Next(0, openDirs.Count() - 1);

        var t = openDirs[p];
        openDirs[p] = openDirs[pp];
        openDirs[pp] = t;
      }
    }
  }

  public class RoomDefinition
  {
    internal Tile GetTile(int i, int j, int id) {
      var result = Definition[i][j];
      result.ID = id;

      return result;
    }

    public Tile[][] Definition { get; set; }
  }

}